//
//  runtime.m
//  iCodeMac
//
//  Created by Andrew Barry on 28/02/09.
//  Copyright 2009 Barry Computer Services. All rights reserved.
//

#import "jsdbgapi.h"
#import "jsinterp.h"
#import "jsscope.h"
#import "jsfun.h"
#import "runtime.h"
#ifdef MACOSX
#import "control.h"
#endif
#import "files.h"
#ifdef MACOSX
#import "form.h"
#endif
#import "WrappedJsval.h"
#ifdef MACOSX
#import "graphics.h"
#endif
#ifdef HASDEBUGGER
#import "DebugClient.h"
#endif
#import "timer.h"
#import "url.h"
#import "keychain.h"
#import "coreAnim.h"
#import "data.h"
#import "task.h"
#import "DebugScript.h"

#ifdef HASDEBUGGER
extern int stepMode;
#endif

int currentLine;
int currentDepth;
int executeDepth;

#ifdef HASDEBUGGER
extern NSDictionary *gInitBreakpoints;
static NSString *gCurrentCompileFilename;
static NSMutableArray *gCurrentCompileBreakpoints;
static NSMutableArray *gCurrentDebugScripts;

NSMutableArray *collectDebugStackFrames(JSContext *cx);
jsval currentException = JSVAL_VOID;
void setScriptBreakpoints(JSScript *script, int startLine, int endLine, NSMutableArray *breakpoints);
#endif

jsval stringToVal(JSContext *cx, NSString *string) {
	int len = [string length];
	unichar *txt = JS_malloc(cx, (len + 1) * sizeof(unichar));
	[string getCharacters:txt];
	
	
	JSString *str = JS_NewUCString(cx, txt, len);
	
	return STRING_TO_JSVAL(str);
}

NSString *valToString(JSContext *cx, jsval string) {
	JSString *str = JS_ValueToString(cx, string);
		
	return [NSString stringWithCharacters: JS_GetStringChars(str) length:JS_GetStringLength(str)];
}

int lastErrorLine;
char *lastErrorMessage;

void errorReporter(JSContext *cx, const char *message, JSErrorReport *report) {
#ifdef HASDEBUGGER
	if (report->flags & JSREPORT_EXCEPTION)
		return;
	debugMessage([NSString stringWithCString: message]);
	debugError(collectDebugStackFrames(cx), message, report->filename, report->lineno);
#endif

	if (lastErrorMessage)
		free(lastErrorMessage);
	lastErrorMessage = strdup(message);
	lastErrorLine = report->lineno;

}
/*
static JSBool
global_resolve(JSContext *context, JSObject *obj, jsval id, uintN flags,
			   JSObject **objp) {
    JSBool resolved;
	
    if ((flags & JSRESOLVE_ASSIGNING) == 0) {
        if (!JS_ResolveStandardClass(context, obj, id, &resolved))
            return JS_FALSE;
        if (resolved) {
            *objp = obj;
            return JS_TRUE;
        }
    }
    *objp = NULL;
    return JS_TRUE;
}
*/
static JSClass global_class = { 
"global",JSCLASS_GLOBAL_FLAGS, 
JS_PropertyStub,JS_PropertyStub,JS_PropertyStub,JS_PropertyStub, 
JS_EnumerateStub,JS_ResolveStub,JS_ConvertStub,JS_FinalizeStub 
};

JSRuntime *rt;
JSContext *cx; 
JSObject *global, *globalProto; 

JSBool HandleDebugStr(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
#ifdef HASDEBUGGER
	for (int idx = 0; idx < argc; idx++) {
		NSString *str = valToString(cx, argv[idx]);
		debugMessage(str);
	}
#endif
    return JS_TRUE;
}

#ifdef HASDEBUGGER
void resetBreakpoints(NSDictionary *newBreakpoints) {
	JS_ClearAllTraps(cx);
	[gInitBreakpoints release];
	gInitBreakpoints = [newBreakpoints retain];
	
	NSString *currentFilename = nil;
	NSMutableArray *currentBreaks = nil;

	for (int idx = 0; idx < [gCurrentDebugScripts count]; idx++) {
		DebugScript *scr = [gCurrentDebugScripts objectAtIndex:idx];
		NSString *filename = [scr filename];
		if (currentFilename == nil || [currentFilename isNotEqualTo:filename]) {
			currentFilename = filename;
			NSArray *breaks = [newBreakpoints objectForKey:filename];
			if (breaks != nil) {
				currentBreaks = [[NSMutableArray arrayWithArray:breaks] autorelease];
			} else {
				currentBreaks = nil;
			}
			
			if (currentBreaks) {
				JSScript *script = [scr script];
				int lineno = [scr lineno];
				int extent = JS_GetScriptLineExtent(cx, script);
				setScriptBreakpoints(script, lineno, lineno+extent, currentBreaks);
			}
		}
	}
}

JSTrapStatus interruptHandler(JSContext *cx, JSScript *script,
						 jsbytecode *pc, jsval *rval, void *closure)
{
	if (stepMode == 1) {
		int nLine = JS_PCToLineNumber(cx, script, pc);
		if (currentLine != nLine || currentDepth != executeDepth) {
			currentLine = nLine;
			currentDepth = executeDepth;
			debugger(collectDebugStackFrames(cx), JS_GetScriptFilename(cx, script), nLine);
		}
	} else if (stepMode == 2) {
		if (currentDepth > executeDepth) {
			int nLine = JS_PCToLineNumber(cx, script, pc);
			currentLine = nLine;
			currentDepth = executeDepth;
			debugger(collectDebugStackFrames(cx), JS_GetScriptFilename(cx, script), nLine);
		} else if (currentDepth == executeDepth) {
			int nLine = JS_PCToLineNumber(cx, script, pc);
			if (currentLine != nLine) {
				currentLine = nLine;
				currentDepth = executeDepth;
				debugger(collectDebugStackFrames(cx), JS_GetScriptFilename(cx, script), nLine);
			}
		}
	}
	return JSTRAP_CONTINUE;
}

void *callHandler(JSContext *cx, JSStackFrame *fp, JSBool before,
				  JSBool *ok, void *closure) {
	if (before) {
		executeDepth++;
	} else {
		executeDepth--;
		if (stepMode == 3 && executeDepth < currentDepth) {
			stepMode = 1;
			currentLine = -1;
		}
		if (executeDepth == 0)
			stepMode = 0;
	}
	
	return cx;
}

NSMutableArray *collectDebugStackFrames(JSContext *cx) {
	NSMutableArray *rv = [NSMutableArray new];
	JSStackFrame *p = nil;

	while (JS_FrameIterator(cx, &p)) {
		JSScript *scr = JS_GetFrameScript(cx, p);
		if (scr) {
			
			NSMutableDictionary *frame = [NSMutableDictionary new];
			NSMutableDictionary *vars = [NSMutableDictionary new];
			NSMutableArray *tempNames = [NSMutableArray new];
			NSMutableArray *varNames = [NSMutableArray new];
			NSMutableArray *varValues = [NSMutableArray new];
			
			NSString *scriptName = [NSString stringWithCString:JS_GetScriptFilename(cx, scr)];
			int nLine = JS_PCToLineNumber(cx, scr, p->pc);
			
			[frame setObject:scriptName forKey:@"script"];
			[frame setObject:[NSNumber numberWithInt:nLine] forKey:@"line"];
			[frame setObject:varNames forKey:@"varNames"];
			[frame setObject:varValues forKey:@"varValues"];

			[rv addObject:frame];
			
			if (p->fun != nil || p->argv != nil) {
				JSObject *funobj = p->argv ? JSVAL_TO_OBJECT(p->argv[-2]) : p->fun->object;
				JSScopeProperty *prop = nil;
				jsval vp = 0;
				
				int varcount  = p->fun->u.i.nvars;
				int argcount = p->argc;

				JSObject *varobj = p->varobj;
				if (varobj == nil)
					varobj = funobj;
				
				if (varobj) {
					JSScopeProperty *pp;
					while (pp = JS_PropertyIterator(funobj, &prop)) {
						JS_IdToValue(cx, pp->id, &vp);
						NSString *str = valToString(cx, vp);
						[tempNames addObject:str];
					}
				}

				for (int varidx = 0; varidx < varcount; varidx++) {
					jsval v = p->vars[varidx];
					NSString *val = valToString(cx, v);
					[varNames addObject:[tempNames objectAtIndex:varcount - varidx - 1]];
					[varValues addObject:val];
				}
				
				for (int varidx = 0; varidx < argcount; varidx++) {
					jsval v = p->argv[varidx];
					NSString *val = valToString(cx, v);
					[varNames addObject:[tempNames objectAtIndex:[tempNames count] - varidx - 1]];
					[varValues addObject:val];
				}
			}
			
			
			//				JS_GetProperty(cx, p->varobj, "x", &vp);
				
/*				
			JSObject *iter = JS_NewPropertyIterator(cx, p->scopeChain);
			jsid vname;
			while (JS_NextProperty(cx, iter, &vname)) {
				if (vname == JSVAL_VOID)
					break;
				JS_IdToValue(cx, vname, &vp);
				NSString *str = valToString(cx, vp);
				jsval v = JSVAL_VOID;
				JS_GetProperty(cx, p->scopeChain, [str UTF8String], &v);
				NSString *val = valToString(cx, v);
				[vars setObject:val forKey:str];
			}
 */
		}
	}
	
	return rv;
}

JSTrapStatus debuggerHandler(JSContext *cx, JSScript *script,
							  jsbytecode *pc, jsval *rval, void *closure)
{
	
	NSMutableArray *stack = collectDebugStackFrames(cx);
	
	currentLine = JS_PCToLineNumber(cx, script, pc);
	currentDepth = executeDepth;
	debugger(stack, JS_GetScriptFilename(cx, script), currentLine);

	return JSTRAP_CONTINUE;
}

JSTrapStatus trapHandler(JSContext *cx, JSScript *script,
									  jsbytecode *pc, jsval *rval, void *closure)
{	
	jsval pendingException = JSVAL_VOID;
	jsval msg = JSVAL_VOID;
	NSString *message = nil;
	
	JS_GetPendingException(cx, &pendingException);
	if (JSVAL_IS_OBJECT(pendingException))
	{
		if (currentException == pendingException)
			return JSTRAP_CONTINUE;
		currentException = pendingException;
		
		JSObject *errorObj = JSVAL_TO_OBJECT(pendingException);
		JS_GetProperty(cx, errorObj, "message", &msg);
		message = valToString(cx, msg);
	}
		
//		JSErrorReport *err = JS_ErrorFromException(cx, pendingException);
	
	if (message)
		debugError(collectDebugStackFrames(cx), [message UTF8String],  JS_GetScriptFilename(cx, script), currentLine);
	else
		debugger(collectDebugStackFrames(cx), JS_GetScriptFilename(cx, script), currentLine);
		
	return JSTRAP_CONTINUE;
}

void newScriptHook(JSContext  *cx,
			const char *filename,
			uintN      lineno,
			JSScript   *script,
			JSFunction *fun,
			void       *callerdata) {

	if (!gCurrentCompileFilename)
		return;

	// track all of the file scripts
	
	if (!gCurrentDebugScripts)
		gCurrentDebugScripts = [NSMutableArray new];
	
	DebugScript *scr = [[DebugScript alloc] initWithFilename: gCurrentCompileFilename withLine: lineno withScript: script];
	[gCurrentDebugScripts addObject: scr];
	
	if (!gCurrentCompileBreakpoints)
		return;

	int extent = JS_GetScriptLineExtent(cx, script);

	setScriptBreakpoints(script, lineno, lineno + extent, gCurrentCompileBreakpoints);	
}
#endif

void stuffGlobal(NSString *name, JSObject *obj) {
	jsval gval = OBJECT_TO_JSVAL(obj);
	JS_DefineProperty(cx, global, [name UTF8String], gval, NULL, NULL, 0);
}

JSBool branchCallback(JSContext *cx, JSScript *script) {
	JS_MaybeGC(cx);
	return JS_TRUE;
}

void initializeRuntime() {
	if (!cx) {
		rt = JS_NewRuntime(0x100000);
		cx = JS_NewContext(rt, 0x1000);

		JS_SetOptions(cx,JS_GetOptions(cx)|JSOPTION_XML);

		JS_SetErrorReporter(cx, errorReporter);
#ifdef HASDEBUGGER
		JS_SetInterrupt(rt, interruptHandler, nil);
		JS_SetThrowHook(rt, trapHandler, nil);
		JS_SetDebuggerHandler(rt, debuggerHandler, nil);
		JS_SetCallHook(rt, callHandler, cx);
		JS_AddRoot(cx, &currentException);
		JS_SetNewScriptHook(rt, newScriptHook, nil);
#endif
		JS_SetBranchCallback(cx, branchCallback);
		
		JS_AddRoot(cx, &global);
		JS_AddRoot(cx, &globalProto);
	}
    global = JS_NewObject(cx, &global_class, NULL, NULL); 

	JS_DefineFunction(cx, global, "debugStr", HandleDebugStr, 1, 0);
	JS_DefineFunction(cx, global, "debugstr", HandleDebugStr, 1, 0);
	
	jsval gval = OBJECT_TO_JSVAL(global);
	JS_DefineProperty(cx, global, "globals", gval, NULL, NULL, JSPROP_READONLY | JSPROP_PERMANENT);
	JS_InitStandardClasses(cx, global);
#ifdef MACOSX
	registerFile(cx, global);
	registerGraphics(cx, global);
	registerTimer(cx, global);
	registerUrl(cx, global);
	registerKeychain(cx, global);
	registerCoreAnim(cx, global);
#ifndef DISABLE_DATABASE
	registerDatabase(cx, global);
#endif
	registerTask(cx, global);
#endif	
	registerForm(cx, global);
	registerData(cx, global);
	registerControls(cx, global);
	JS_GC(cx);
}

void runScript(NSString *filename, NSString *script) {
//	ok = JS_ExecuteScript(cx, JS_GetGlobalObject(cx), compiledScript, &rval);

	jsval rval;
	
//	const char *strScript = [script UTF8String];
	
#ifdef HASDEBUGGER
	NSArray *fileBreakpoints = [gInitBreakpoints objectForKey:filename];
	gCurrentCompileFilename = filename;
	if (fileBreakpoints)
		gCurrentCompileBreakpoints = [NSMutableArray arrayWithArray:fileBreakpoints];
#endif
	
	int len = [script length];
	unichar *scriptText = (unichar *) malloc((len + 1) * sizeof(unichar));
	[script getCharacters:scriptText];
	scriptText[len] = 0;
	
	JSScript *compiled = JS_CompileUCScript(cx, JS_GetGlobalObject(cx), scriptText, len, [filename UTF8String], 1);
	
	free(scriptText);
	
#ifdef HASDEBUGGER
	gCurrentCompileBreakpoints = nil;
	gCurrentCompileFilename = nil;
#endif
	
	JSObject *scrobj = JS_NewScriptObject(cx, compiled);
	WrappedJsval *scriptVal = [[WrappedJsval alloc] initWithVal:OBJECT_TO_JSVAL(scrobj)];
	
	JS_ExecuteScript(cx, global, compiled, &rval);
}

JSObject *newCoreObject() {
	return JS_NewObject(cx, &global_class, globalProto, NULL);
}

NSMutableDictionary *gLoadedScripts;

void compileEventHandler(NSString *objectName, NSString *eventName, NSDictionary *dict) {
	NSString *script = [dict objectForKey:eventName];
	if (!script)
		return;
	NSString *scriptFile = [NSString stringWithFormat:@"%@/%@", objectName, eventName];

#ifdef HASDEBUGGER
	NSArray *eventBreakpoints = [gInitBreakpoints objectForKey:scriptFile];
	gCurrentCompileFilename = scriptFile;
	if (eventBreakpoints)
		gCurrentCompileBreakpoints = [NSMutableArray arrayWithArray:eventBreakpoints];

#endif
	int len = [script length];
	unichar *scriptText = (unichar *) malloc((len + 1) * sizeof(unichar));
	[script getCharacters:scriptText];
	scriptText[len] = 0;
	JSFunction *compiledScript = JS_CompileUCFunction( cx, nil, nil, 0, nil, scriptText, len, [scriptFile UTF8String], 1);
	free(scriptText);
#ifdef HASDEBUGGER
	gCurrentCompileBreakpoints = nil;
	gCurrentCompileFilename = nil;
#endif	
	if (!compiledScript)
		return;
	jsval sval = OBJECT_TO_JSVAL(JS_GetFunctionObject(compiledScript));
	
	if (!gLoadedScripts)
		gLoadedScripts = [NSMutableDictionary new];
	
	WrappedJsval *scriptVal = [[WrappedJsval alloc] initWithVal:sval];

	[gLoadedScripts setObject:scriptVal forKey:scriptFile];

	[dict setValue:scriptVal forKey:eventName];
}

void applyEventHandler(JSObject *obj, NSString *eventName, NSDictionary *dict) {
	WrappedJsval *handler = [dict objectForKey:eventName];
	if (handler) {
		JS_DefineProperty(cx, obj, [eventName UTF8String], handler->val, NULL, NULL, 0);
	}
}
#ifdef HASDEBUGGER
void setScriptBreakpoints(JSScript *script, int startLine, int endLine, NSMutableArray *breakpoints) {
	if (!breakpoints)
		return;

	int idx = 0;
	while (idx < [breakpoints count]) {
		int nLine = [[breakpoints objectAtIndex:idx] intValue];
		if (nLine >= startLine && nLine <= endLine) {
			jsbytecode *pc = JS_LineNumberToPC(cx, script, nLine);
			if (pc)
				JS_SetTrap(cx, script, pc,  debuggerHandler, nil);
			[breakpoints removeObjectAtIndex:idx];
		} else {
			idx++;
		}
	}
}
#endif
